package shell;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import shell.command.EchoCommandImpl;
import shell.command.HelpCommandImpl;
import shell.command.ShellCommand;
import shell.command.ShellContext;
import shell.command.ShellContextImpl;
import shell.command.VarCommandImpl;

/**
 * Shell de conversation.
 * 
 * @version 1.0
 */
public class Shell implements Runnable, ShellConstantes {
    /**
     * Retour chariot.
     */
    public static final String RETOUR_CHARIOT = System.getProperties()
	    .getProperty("line.separator");

    /**
     * Liste des commandes possibles.
     */
    private Map<String, ShellCommand> nomsCommandes = new TreeMap<String, ShellCommand>();

    /**
     * Contexte d'exécution.
     */
    private ShellContext contexte = new ShellContextImpl(this.nomsCommandes);

    /**
     * Flux d'entrée.
     */
    private InputStream in;

    /**
     * Flux de sortie.
     */
    private PrintStream out;

    /**
     * Flux d'erreurs.
     */
    private PrintStream err;

    /**
     * Arrêt sur erreur.
     */
    private boolean arretErreur = false;

    /**
     * Présence d'erreur.
     */
    private Exception erreur;

    /**
     * Démarrage d'un shell.
     * 
     * @param args
     *            Arguments de lancement
     */
    public static void main(final String args[]) {
	new Shell(args).run();
    }

    /**
     * Constructeur du shell.
     * 
     * @param args
     *            Arguments de lancement
     */
    public Shell(final String args[]) {
	/*
	 * Ouverture des flux
	 */
	this.out = System.out;
	this.err = System.err;

	/*
	 * Déclaration du contexte
	 */
	this.getContexte().setVar("stop", Boolean.FALSE);

	/*
	 * Allocation du flux d'entrée
	 */
	if (!this.parseArgs(args)) {
	    this.stop();
	}

	ShellContext context = getContexte();

	/*
	 * Ajout de la commande "contexte".
	 */
	this.addCommand(new VarCommandImpl(context));

	/*
	 * Ajout de la commande "echo".
	 */
	this.addCommand(new EchoCommandImpl(context));

	/*
	 * Ajout de la commande "help".
	 */
	this.addCommand(new HelpCommandImpl(context));
	this.addCommand(new HelpCommandImpl(context), "?");

	/*
	 * Ajout de la commande "quit".
	 */
	this.addCommand(new shell.command.SimpleQuitCommandImpl(context));
	this.addCommand(new shell.command.SimpleQuitCommandImpl(context),
		"exit");

	/*
	 * Ajout de la commande "run".
	 */
	this.addCommand(new shell.command.RunCommandImpl(context));

	/*
	 * Définition des variables d'environnement.
	 */
	context.setVar(ShellConstantes.PROMPT, ">");
	context.setVar(ShellConstantes.BANNER, this.getBanner());
    }

    /**
     * Exécution du shell, après instanciation.
     */
    public final void run() {
	/*
	 * Variable de traitement
	 */
	String line = null;
	String prompt = null;
	BufferedReader br = null;

	/*
	 * Traitement de démarrage
	 */
	if (!this.isStop()) {
	    /*
	     * Récupération de la bannière
	     */
	    String banner = (String) this.contexte.getVar(BANNER);
	    if (banner != null) {
		this.out.println(banner);
	    }

	    /*
	     * Récupération du prompt
	     */
	    prompt = (String) this.contexte.getVar(PROMPT);
	    if (prompt == null) {
		prompt = ">";
	    }

	    br = new BufferedReader(new InputStreamReader(this.in));
	}

	/*
	 * Traitement tant que le signal stop n'est pas levé
	 */
	while (!this.isStop()) {
	    this.out.print(prompt);

	    try {
		line = br.readLine();

		if (line != null) {
		    line = line.trim();

		    if (line.length() != 0) {
			try {
			    executerCommande(line, this.out, this.err);
			} catch (final Exception e) {
			    this.err.println("Exception shell: " + e);
			}
		    }
		}
	    } catch (final IOException e) {
		this.err.println("Impossible de lire la commande : "
			+ e.getMessage());
	    }
	}
    }

    /**
     * Obtention de la bannière au lancement.
     * 
     * @return La bannière
     */
    public String getBanner() {
	/*
	 * Variable de retour
	 */
	StringBuffer retour = new StringBuffer();

	retour.append("+-----------------------------------------------+");
	retour.append(Shell.RETOUR_CHARIOT);
	retour.append("! Shell générique                               !");
	retour.append(Shell.RETOUR_CHARIOT);
	retour.append("+-----------------------------------------------+");

	/*
	 * Sortie
	 */
	return retour.toString();
    }

    /**
     * Getter de l'attribut contexte.
     * 
     * @return L'attribut contexte
     */
    public final ShellContext getContexte() {
	return this.contexte;
    }

    /**
     * Parcours des paramètres fournis.
     * 
     * @param args
     *            Les paramètres fournis
     * @return True si les paramètres sont corrects, faux sinon
     */
    public boolean parseArgs(final String args[]) {
	/*
	 * Variable de retour
	 */
	boolean retour = true;

	String[] arguments = args.clone();

	if (arguments.length < 1) {
	    /*
	     * Si pas d'arguments, entrée console
	     */
	    this.in = System.in;
	} else {
	    /*
	     * Sinon entrée fichier
	     */
	    try {
		this.in = new FileInputStream(arguments[0]);
	    } catch (final FileNotFoundException e) {
		this.err.println("Fichier \"" + arguments[0]
			+ "\"non trouvé : " + e.getMessage());
		retour = false;
	    }

	}

	/*
	 * Sortie
	 */
	return retour;
    }

    /**
     * Indique si la console doit être arrêtée.
     * 
     * @return True si la console doit être arrêtée, False sinon
     */
    protected final Boolean isStop() {
	return (Boolean) this.getContexte().getVar("stop");
    }

    /**
     * Arrêt de la console.
     */
    protected final void stop() {
	this.getContexte().setVar("stop", Boolean.TRUE);
    }

    /**
     * Exécution d'une commande dans le shell.
     * 
     * @param ligneCommande
     *            La ligne de commande en entrée
     * @param out
     * @param err
     * @throws Exception
     */
    protected final synchronized void executerCommande(
	    final String ligneCommande, final PrintStream out,
	    final PrintStream err) {
	/*
	 * Variable de traitement
	 */
	String commandeStr = ligneCommande.trim();
	String nomCommande = null;
	int position;

	/*
	 * Recherche du nom de la commande et suppression de celui-ci dans la
	 * ligne de commande
	 */
	position = commandeStr.indexOf(' ');
	if (position >= 0) {
	    nomCommande = commandeStr.substring(0, position);
	    if (commandeStr.length() >= position + 1) {
		commandeStr = commandeStr.substring(position + 1);
	    }
	} else {
	    nomCommande = commandeStr;
	    commandeStr = "";
	}
	ShellCommand commande = this.getCommand(nomCommande);

	/*
	 * Execution de la commande
	 */
	if (commande != null) {
	    try {
		commande.execute(commandeStr, out, err);
	    } catch (final RuntimeException e) {
		erreur = e;
		if (this.arretErreur) {
		    this.stop();
		}
	    } catch (final Exception e) {
		erreur = e;
		if (this.arretErreur) {
		    this.stop();
		}
	    }
	} else {
	    err.println("Commande inconnue");
	}
    }

    /**
     * Getter de la commande correspondant au nom.
     * 
     * @param name
     *            Le nom de la commande
     * @return La commande correspondante
     */
    protected final synchronized ShellCommand getCommand(final String name) {
	/*
	 * Variable de retour
	 */
	ShellCommand retour = null;

	retour = this.nomsCommandes.get(name);

	/*
	 * Sortie
	 */
	return retour;
    }

    /**
     * Ajoute une commande aux commandes possibles.
     * 
     * @param commande
     *            La commande à ajouter
     */
    protected final synchronized void addCommand(final ShellCommand commande) {
	this.nomsCommandes.put(commande.getName(), commande);
    }

    /**
     * Ajoute une commande aux commandes possibles.
     * 
     * @param commande
     *            La commande à ajouter
     * @param nom
     *            Le nom de la commande
     */
    protected final synchronized void addCommand(final ShellCommand commande,
	    final String nom) {
	this.nomsCommandes.put(nom, commande);
    }

    /**
     * Enlève une commande des commandes possibles.
     * 
     * @param nom
     *            Le nom de la commande à enlever
     */
    protected final synchronized void removeCommand(final String nom) {
	this.out.println("Commande retirée : " + nom);
	this.nomsCommandes.remove(nom);
    }

    /**
     * Supprime toutes les commandes possibles.
     */
    protected final synchronized void clearCommands() {
	this.nomsCommandes.clear();
    }

    /**
     * Getter de l'attribut nomsCommandes.
     * 
     * @return the nomsCommandes
     */
    public Map<String, ShellCommand> getNomsCommandes() {
	return this.nomsCommandes;
    }

    /**
     * Setter de l'attribut nomsCommandes.
     * 
     * @param nomsCommandes
     *            L'attribut nomsCommandes à valoriser
     */
    public void setNomsCommandes(Map<String, ShellCommand> nomsCommandes) {
	this.nomsCommandes = nomsCommandes;
    }

    /**
     * Getter de l'attribut in.
     * 
     * @return the in
     */
    public InputStream getIn() {
	return this.in;
    }

    /**
     * Setter de l'attribut in.
     * 
     * @param in
     *            L'attribut in à valoriser
     */
    public void setIn(InputStream in) {
	this.in = in;
    }

    /**
     * Getter de l'attribut out.
     * 
     * @return the out
     */
    public PrintStream getOut() {
	return this.out;
    }

    /**
     * Setter de l'attribut out.
     * 
     * @param out
     *            L'attribut out à valoriser
     */
    public void setOut(PrintStream out) {
	this.out = out;
    }

    /**
     * Getter de l'attribut err.
     * 
     * @return the err
     */
    public PrintStream getErr() {
	return this.err;
    }

    /**
     * Setter de l'attribut err.
     * 
     * @param err
     *            L'attribut err à valoriser
     */
    public void setErr(PrintStream err) {
	this.err = err;
    }

    /**
     * Setter de l'attribut contexte.
     * 
     * @param contexte
     *            L'attribut contexte à valoriser
     */
    public void setContexte(ShellContext contexte) {
	this.contexte = contexte;
    }

    /**
     * Getter de l'attribut arretErreur.
     * 
     * @return the arretErreur
     */
    public boolean isArretErreur() {
	return this.arretErreur;
    }

    /**
     * Setter de l'attribut arretErreur.
     * 
     * @param arretErreur
     *            L'attribut arretErreur à valoriser
     */
    public void setArretErreur(boolean arretErreur) {
	this.arretErreur = arretErreur;
    }

    /**
     * Getter de l'attribut erreur.
     * 
     * @return the erreur
     */
    public boolean isErreur() {
	return erreur != null;
    }

    /**
     * Getter de l'attribut erreur.
     * 
     * @param erreur
     *            L'attribut erreur à valoriser
     */
    public Exception getErreur() {
	return this.erreur;
    }

    /**
     * Extraction des arguments d'une chaîne de caractère. La délimitation est
     * faite par les espaces ou les guillemets. .
     * 
     * @param cmdline
     *            La chaîne de caractères
     * @return La liste des arguments sous forme de chaîne de caractères
     */
    public static final List<String> analyserCommande(final String cmdline) {
	/*
	 * Variable de retour
	 */
	List<String> retour = new ArrayList<String>();

	/*
	 * Variable de balayage
	 */
	StringBuffer temp = new StringBuffer();
	long longueur = cmdline.length();
	char courant;
	boolean guillemet = false;

	/*
	 * Balayage des caractères
	 */
	for (int i = 0; i < longueur; ++i) {
	    courant = cmdline.charAt(i);

	    /*
	     * Détection de la fin d'un argument
	     */
	    if ((!guillemet && courant == ' ' && temp.length() > 0)
		    || (guillemet && courant == '"')) {
		retour.add(temp.toString());
		temp = new StringBuffer();
		guillemet = false;
	    } else {
		/*
		 * Détection du début
		 */
		if (courant == '"') {
		    guillemet = true;
		} else {
		    /*
		     * Espace ignoré si rien avant
		     */
		    if (courant != ' '
			    || (courant == ' ' && (temp.length() > 0 || guillemet))) {
			temp.append(courant);
		    }
		}
	    }
	}

	/*
	 * Ajout de la dernière chaîne
	 */
	if (temp.length() > 0) {
	    retour.add(temp.toString());
	}

	/*
	 * Sortie
	 */
	return retour;
    }
}
